﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using log4net;
using System.Xml;
using System.IO;
using System.Reflection;

namespace PreCommitHook
{
    /// <summary>
    /// Analizes the collected code coverage data.
    /// checks if the file and assembly are following the given policy in codeCoverageSettings.xml
    /// calculates the code coverage for assembly and file
    /// </summary>
    public class MSCodeCoverageParser : ICodeCoverageParser
    {
        ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public IList<IUnitTestMetada> FailedTestsList { get; set; }
        public string UnitTestResultsFilePath { get; set; }
        public string CoverageFilePath { get; set; }
        public MSCodeCoverageParser()
        {
            FailedTestsList = new List<IUnitTestMetada>();
        }


        /// Checks the code coverage threshold from  codeCoverageSettings.xml and compares them against values in generated CoverageFilePath .xml 
        /// <summary>
        /// <param name="assemblyToCheck">which assembly to checks for code coverage</param>
        /// <param name="fileToCommit">the file which user is trying to commit</param>
        /// <param name="username">the username who performs the commit</param>
        /// <returns>false if there is a file that doesn't meet required code coverage result, true otherwise</returns>
        public bool CheckTrashHoldCoverage(string assemblyToCheck, string fileToCommit, string username)
        {
            log.Info(String.Format("Assembly {0} and file {1} are going to be check for code coverage threshold according values given in codeCoverageSettings.xml.", assemblyToCheck, fileToCommit));

            if (String.IsNullOrEmpty(assemblyToCheck) || String.IsNullOrEmpty(fileToCommit) || String.IsNullOrEmpty(username))
            {
                log.Fatal(String.Format("CheckTrashHoldCoverage method has been called with invalid arguments: null or empty string. Passed values Assembly:{0}, fileToCommit:{1}, username:{2}.", assemblyToCheck, fileToCommit, username));
                return false;

            }
            //get the username from svnUsers.xml config and see if this user should be checked on commits
            SubMethodResult result = ShouldCheckConfigUsers(username);
            switch (result)
            {
                case SubMethodResult.BreakFalse:
                    {
                        log.Error(String.Format("There is no record or there is more than one user with username '{0}' in svnUsers.xml config file", username));
                        return false;
                    }
                case SubMethodResult.BreakTrue:
                    {
                        log.Info(String.Format("Username {0} should not be checked for code coverage and no code coverage check for results is performed for file {1} in assembly {2}.", username, fileToCommit, assemblyToCheck));
                        return true;
                    }
            }

            result = IsAssemblyAboveRequiredCodeCoverage(assemblyToCheck);
            switch (result)
            {
                case SubMethodResult.BreakFalse:
                    {
                        return false;
                    }
                case SubMethodResult.BreakTrue:
                    {
                        return true;
                    }
            }

            result = IsClassAboveRequiredCodeCoverage(fileToCommit, assemblyToCheck);
            switch (result)
            {
                case SubMethodResult.BreakFalse:
                    {
                        return false;
                    }
                case SubMethodResult.BreakTrue:
                    {
                        return true;
                    }
            }

            return true;
        }

        /// <summary>
        /// loads trx file and check is there failed test
        /// </summary>
        /// <returns>true if all tests are executed successfully, false otherwise</returns>
        public bool AreAllTestCasesSuccessful()
        {

            if (String.IsNullOrEmpty(UnitTestResultsFilePath))
            {
                log.Fatal("UnitTestResultsFilePath path to the test results is not initialized");
                return false;
            }
            log.Info("AreAllTestCasesSuccessful called");
            try
            {
                XDocument xDoc = XDocument.Load(UnitTestResultsFilePath);

                //strip the namespaces in order to make our linq to xml more "human readable"
                LinqToXmlHelper.StripNamespaces(xDoc.Root);

                var userDeploymentRoot = (from el in xDoc.Root.Elements("TestRunConfiguration").Elements()
                                          where el.Name.LocalName.Equals("Deployment")
                                          select new { value = el.Attribute("userDeploymentRoot").Value });

                var valueEl = userDeploymentRoot.ToList();
                if (!String.IsNullOrEmpty(valueEl[0].value))
                    log.Info(String.Format("User deployment root: {0}", valueEl[0].value));


                var resultElement = (from xmlEl in xDoc.Root.Elements("ResultSummary") select xmlEl);
                List<XElement> descendants = resultElement.Descendants().ToList();
                

                Int32 total = Convert.ToInt32(descendants[0].Attribute("total").Value);
                Int32 executed = Convert.ToInt32(descendants[0].Attribute("executed").Value);
                Int32 passed = Convert.ToInt32(descendants[0].Attribute("passed").Value);

                log.Info(String.Format("AreAllTestCasesSuccessful():based on passed test result file: {0} - cases have run, {1} - executed, {2} - successful", total, executed, passed));

                //get exceptions and log them
                if (passed != total || passed != executed)
                {
                    //get all failed exception - outcome != Passed and store List of UnitTestMetada in UnitTestMetada
                    var notPassed = (from testResult in xDoc.Root.Elements("Results").Elements()
                                     where (string)testResult.Attribute("outcome") != "Passed"
                                     select new
                                     {
                                         testID = (string)testResult.Attribute("testId"),
                                         status = (string)testResult.Attribute("outcome"),
                                         testName = (string)testResult.Attribute("testName")

                                     });

                    foreach (var failedTest in notPassed.ToList())
                    {
                        var testMetaData = (from testDefinition in xDoc.Root.Elements("TestDefinitions").Elements()
                                            where (string)testDefinition.Attribute("id") == failedTest.testID && (string)testDefinition.Attribute("name") == failedTest.testName
                                            select new
                                                {
                                                    Description = testDefinition.Element("Description").Value,
                                                    Status = failedTest.status,
                                                    TestAssembly = testDefinition.Element("TestMethod").Attribute("codeBase").Value,
                                                    ClassName = testDefinition.Element("TestMethod").Attribute("className").Value,
                                                    TestName = failedTest.testName,
                                                    TestID = failedTest.testID
                                                }
                                        );


                        if (testMetaData.ToList().Count != 1)
                        {
                            log.Error(String.Format("There is more than one test {0} with id {1} in the tests result file.", testMetaData.ToList()[0].TestName, testMetaData.ToList()[0].TestID));
                            return false;
                        }

                        FailedTestsList.Add(new UnitTestMetada()
                        {
                            AssemblyName = testMetaData.ToList()[0].TestAssembly,
                            Description = testMetaData.ToList()[0].Description,
                            Name = testMetaData.ToList()[0].TestName,
                            TestID = testMetaData.ToList()[0].TestID,
                            ClassName = testMetaData.ToList()[0].ClassName,
                            Status = testMetaData.ToList()[0].Status
                        });

                    }

                    return false;
                }
            }
            catch (XmlException ex)
            {
                log.Fatal(String.Format("XmlException error on loading test results file {0}.", UnitTestResultsFilePath), ex);
                return false;
            }
            catch (Exception ex)
            {
                log.Fatal(String.Format("Error on loading test results file {0}.", UnitTestResultsFilePath), ex);
                return false;
            }
            return true;
        }

        #region helper methods

        private static double GetPercentCoverage(int dblCovered, int dblNot)
        {
            int dblTotal = dblCovered + dblNot;
            return 100.0 * (double)dblCovered / (double)dblTotal;
        }

        internal SubMethodResult ShouldCheckConfigUsers(string username)
        {
            try
            {
                log.Info(String.Format("ShouldCheckConfigUsers method called for user:'{0}'", username));
                string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                XDocument xDoc = XDocument.Load(path + @"\Config\svnUsers.xml");
                var userElement = (from user in xDoc.Root.Elements()
                                   where user.Value.Equals(username)
                                   select user);


                List<XElement> users = userElement.ToList();
                if (users.Count != 1)
                {
                    throw new ApplicationException(String.Format("There is no record or there is more than one user with username '{0}' in svnUsers.xml config file", username));
                    //return SubMethodResult.BreakFalse;
                }

                XElement userToCommit = users[0];
                if (userToCommit.Attribute("obeyPreCommitRules") != null)
                {
                    if ("false" == (string)userToCommit.Attribute("obeyPreCommitRules").Value)
                    {
                        return SubMethodResult.BreakTrue;
                    }
                }

                return SubMethodResult.Continue;
            }
            catch (XmlException ex)
            {
                log.Fatal("XmlException error on loading svnUsers.xml.", ex);
                return SubMethodResult.BreakFalse;
            }
            catch (Exception ex)
            {
                log.Fatal("Exception in ShouldCheckConfigUsers.", ex);
                return SubMethodResult.BreakFalse;
            }

        }

        internal bool ShouldCheckAssembly(string assemblyToCheck)
        {
            try
            {
                string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                XDocument xDoc = XDocument.Load(path + @"\Config\codeCoverageSettings.xml");

                var query = (from assemblyEl in xDoc.Root.Elements()
                             where assemblyEl.Attribute("name").Value.Equals(assemblyToCheck)
                             select assemblyEl);

                List<XElement> assemblyElements = query.ToList();

                if (assemblyElements.Count != 1)
                {
                    log.Error(String.Format("There is no record or there is more than one assembly with name '{0}' in codeCoverageSettings.xml config file", assemblyToCheck));
                    throw new Exception(String.Format("There is no record or there is more than one assembly with name '{0}' in codeCoverageSettings.xml config file", assemblyToCheck));
                }

                XElement assembly = assemblyElements[0];
                if (assembly.Attribute("check").Value.Equals("false"))
                {
                    log.Info(String.Format("Assembly {0} should not be checked for code coverage and no code coverage check for results is performed.", assemblyToCheck));
                    return false;
                }

                return true;
            }
            catch (Exception ex)
            {
                log.Fatal(String.Format("XmlException error on loading codeCoverageSettings.xml for assembly {0}", assemblyToCheck), ex);
                throw;
            }

        }
        private SubMethodResult IsAssemblyAboveRequiredCodeCoverage(string assemblyToCheck)
        {
            log.Info(String.Format("Assembly {0} is going to be checked for the code coverage threshold.", assemblyToCheck));
            double assemblyExpectedCodeCoverage = 0d;
            double assemblyCalculatedCodeCoverage = 0d;

            try
            {
                string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                XDocument xDoc = XDocument.Load(path + @"\Config\codeCoverageSettings.xml");

                var query = (from assemblyEl in xDoc.Root.Elements()
                             where assemblyEl.Attribute("name").Value.Equals(assemblyToCheck)
                             select assemblyEl);

                List<XElement> assemblyElements = query.ToList();

                if (assemblyElements.Count != 1)
                {
                    log.Error(String.Format("There is no record or there is more than one assembly with name '{0}' in codeCoverageSettings.xml config file", assemblyToCheck));
                    return SubMethodResult.BreakFalse;
                }

                XElement assembly = assemblyElements[0];
                if (assembly.Attribute("check").Value.Equals("false"))
                {
                    log.Info(String.Format("Assembly {0} should not be checked for code coverage and no code coverage check for results is performed.", assemblyToCheck));
                    return SubMethodResult.BreakTrue;
                }
                assemblyExpectedCodeCoverage = Convert.ToDouble(assembly.Attribute("covered").Value);

                xDoc = XDocument.Load(CoverageFilePath);
                var qAssembly = (from el in xDoc.Root.Elements("Module").Elements()
                                 where el.Value.Equals(assemblyToCheck)
                                 select el.Parent);

                List<XElement> codeCoverageAssembly = qAssembly.ToList();
                if (codeCoverageAssembly.Count != 1)
                {
                    log.Error(String.Format("There is no record or there is more than one assembly with name '{0}' in generated code coverage file {1}", assemblyToCheck, CoverageFilePath));
                    return SubMethodResult.BreakFalse;
                }
                XElement module = codeCoverageAssembly[0];
                var blocksCovered = (from m in module.Elements()
                                     where m.Name.LocalName.Equals("BlocksCovered")
                                     select m);

                var blocksNotCovered = (from m in module.Elements()
                                        where m.Name.LocalName.Equals("BlocksNotCovered")
                                        select m);


                assemblyCalculatedCodeCoverage = GetPercentCoverage(Convert.ToInt32((blocksCovered.ToList()[0] as XElement).Value), Convert.ToInt32((blocksNotCovered.ToList()[0] as XElement).Value));

                if (assemblyCalculatedCodeCoverage < assemblyExpectedCodeCoverage)
                {
                    log.Info(String.Format("Assembly '{0}' has coverage less than the calculated one: {1}", assemblyToCheck, assemblyCalculatedCodeCoverage));
                    return SubMethodResult.BreakFalse;
                }

                return SubMethodResult.Continue;
            }
            catch (XmlException ex)
            {
                log.Fatal(String.Format("XmlException error on loading codeCoverageSettings.xml or {0}", CoverageFilePath), ex);
                return SubMethodResult.BreakFalse;
            }
            catch (Exception ex)
            {
                log.Fatal("Exception in IsAssemblyAboveRequiredCodeCoverage.", ex);
                return SubMethodResult.BreakFalse;
            }

        }

        private SubMethodResult IsClassAboveRequiredCodeCoverage(string fileToCommit, string assemblyToCheck)
        {
            log.Info(String.Format("Class {0} from assembly {1} is going to be checked for code coverage threshold.", fileToCommit, assemblyToCheck));
            try
            {
                XDocument xDoc = XDocument.Load(CoverageFilePath);

                var qAssembly = (from el in xDoc.Root.Elements("Module").Elements()
                                 where el.Value.Equals(assemblyToCheck)
                                 select el.Parent);


                var methodquery = (from m in qAssembly.Descendants("Class").Elements()
                                   where m.Name.LocalName.Equals("ClassName") && m.Value.Equals(fileToCommit)
                                   select m);

                if (methodquery.ToList().Count != 1)
                {
                    log.Info(String.Format("No file with file name '{0}' found in code coverage result file '{1}' for assembly '{2}'.", fileToCommit, CoverageFilePath, assemblyToCheck));
                    return SubMethodResult.BreakFalse;
                }

                var q = (from method in qAssembly.Descendants("Class").Elements()
                         where method.Name.LocalName.Equals("ClassName") && method.Value.Equals(fileToCommit)
                         select method.ElementsAfterSelf().ToList()
                          );

                XElement methodsBlocksCovered = null;
                XElement methodsBlocksNotCovered = null;
                foreach (XElement x in q.ToList()[0].ToList())
                {
                    if (x.Name.LocalName.Equals("BlocksCovered"))
                        methodsBlocksCovered = x;
                    if (x.Name.LocalName.Equals("BlocksNotCovered"))
                        methodsBlocksNotCovered = x;

                    if (methodsBlocksCovered != null && methodsBlocksNotCovered != null) break;
                }

                double classPercentCoverage = GetPercentCoverage(Convert.ToInt32(methodsBlocksCovered.Value), Convert.ToInt32(methodsBlocksNotCovered.Value));

                //class coverage in codeCoverageSettings
                string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                xDoc = XDocument.Load(path + @"\Config\codeCoverageSettings.xml");

                var queryConfigClass = (from assemblyEl in xDoc.Root.Elements()
                                        where assemblyEl.Attribute("name").Value.Equals(assemblyToCheck)
                                        select assemblyEl.Elements("class").Where(e => (string)e.Attribute("name").Value == fileToCommit).FirstOrDefault());

                List<XElement> configClass = queryConfigClass.ToList();
                if (configClass.Count != 1)
                {
                    log.Info(String.Format("No class or more than one class with name '{0}' found in code codeCoverageSettings.xml  for assembly '{1}'.", fileToCommit, assemblyToCheck));
                    return SubMethodResult.BreakFalse;
                }

                if (configClass[0].Attribute("check").Value.Equals("false"))
                {
                    log.Info(String.Format("Class:'{0}'in assembly:'{1}' in codeCoverageSettings.xml should not be checked for coverage results and no checks is performed", fileToCommit, assemblyToCheck));
                    return SubMethodResult.BreakTrue;
                }

                double expectedClassCoverage = Convert.ToDouble(configClass[0].Attribute("covered").Value);

                if (classPercentCoverage < expectedClassCoverage)
                {
                    log.Info(String.Format("Expected code coverage for class '{0}' is less than the calculated {1}.", fileToCommit, classPercentCoverage));
                    return SubMethodResult.BreakFalse;
                }
                return SubMethodResult.Continue;
            }
            catch (XmlException ex)
            {
                log.Fatal(String.Format("XmlException error on loading '{0}' or codeCoverageSettings.xml.", CoverageFilePath), ex);
                return SubMethodResult.BreakFalse;
            }
            catch (Exception ex)
            {
                log.Fatal("Exception on IsClassAboveRequiredCodeCoverage.", ex);
                return SubMethodResult.BreakFalse;
            }
        }
        #endregion
    }
}
